//-----------------------------------------------------------------------------
// <copyright file="PluginManager.cs" company="Justin Wallace Saunders">
//     Copyright (c) Microsoft Public License. All rights reserved.
// </copyright>
// <author name="Justin Wallace Saunders" email="justinsa@live.ca" />
//-----------------------------------------------------------------------------
namespace Community.Tools.Plugins
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Reflection;

    /// <summary>
    /// Collects and manages plugins, notifying listeners about
    /// the state of plugins within the collection.
    /// </summary>
    public class PluginManager
    {
        /// <summary>
        /// Initializes a new instance of the PluginManager class.
        /// </summary>
        public PluginManager()
        {
            this.Plugins = new Dictionary<string, IPlugin>();
            this.PluginTypeFilter = new List<Type>();
            this.PluginTypeFilter.Add(typeof(IPlugin));
        }

        /// <summary>
        /// An event thrown when a plugin is loaded into the collection.
        /// </summary>
        public event EventHandler<PluginLoadedEventArgs> PluginLoadedEvent;

        /// <summary>
        /// An event thrown when a plugin is unloaded from the collection.
        /// </summary>
        public event EventHandler<PluginUnloadedEventArgs> PluginUnloadedEvent;

        /// <summary>
        /// Gets the number of elements in the collection.
        /// </summary>
        public int Count
        {
            get
            {
                return this.Plugins.Count;
            }
        }

        /// <summary>
        /// Gets or sets the plugin collection.
        /// </summary>
        private IDictionary<string, IPlugin> Plugins { get; set; }

        /// <summary>
        /// Gets or sets the types of plugin interfaces to allow in this collection.
        /// </summary>
        private ICollection<Type> PluginTypeFilter { get; set; }

        /// <summary>
        /// Resolves the status of the existence of objects in the collection.
        /// </summary>
        /// <returns>True if there is an object in the collection, false otherwise.</returns>
        public bool IsEmpty()
        {
            return this.Plugins.Count == 0;
        }

        /// <summary>
        /// Gets the plugin from the collection with the provided <paramref name="key"/>.
        /// </summary>
        /// <param name="key">The <paramref name="key"/> of the plugin to get.</param>
        /// <returns>The plugin or null if the <paramref name="key"/> is not in the collection.</returns>
        public IPlugin GetPlugin(string key)
        {
            return this.Plugins[key];
        }

        /// <summary>
        /// Adds a required filter to the collection. Plugins loaded and currently held must
        /// support all Interface types in the filter.
        /// </summary>
        /// <param name="type">The Interface type to allow in the collection.</param>
        /// <exception cref="System.ArgumentNullException">Thrown when <paramref name="type"/> is null.</exception>
        /// <exception cref="System.ArgumentException">Thrown when <paramref name="type"/> is not an Interface.</exception>
        public void AddFilter(Type type)
        {
            if (type == null)
            {
                throw new ArgumentNullException("type");
            }

            if (!type.IsInterface)
            {
                throw new ArgumentException("Filter must be an Interface", "type");
            }

            if (!this.PluginTypeFilter.Contains(type))
            {
                this.PluginTypeFilter.Add(type);
                this.Filter();
            }
        }

        /// <summary>
        /// Load an IPlugin or collection of IPlugins.
        /// </summary>
        /// <param name="path">The path to a directory or file.</param>
        /// <param name="recursive">Whether to recursively go through sub-directories.</param>
        /// <exception cref="System.NullReferenceException">Thrown when <paramref name="path"/> is null.</exception>
        /// <exception cref="System.ArgumentException">Thrown when <paramref name="path"/> is not a valid path to a directory or file.</exception>
        public void Load(string path, bool recursive)
        {
            if (string.IsNullOrEmpty(path))
            {
                throw new ArgumentNullException("path");
            }

            if (Directory.Exists(path))
            {
                this.LoadDirectory(path, recursive);
            }
            else if (File.Exists(path))
            {
                this.LoadFile(path);
            }
            else
            {
                throw new ArgumentException("path must be a valid path to a directory or file.", "path");
            }
        }

        /// <summary>
        /// Loads all IPlugins from the directory.
        /// </summary>
        /// <param name="path">The path to a directory containing DLLs.</param>
        /// <param name="recursive">Whether to recursively go through sub-directories.</param>
        /// <exception cref="System.NullReferenceException">Thrown if <paramref name="path"/> is null.</exception>
        /// <exception cref="System.ArgumentException">Thrown if the <paramref name="path"/> is not a valid directory.</exception>
        public void LoadDirectory(string path, bool recursive)
        {
            if (string.IsNullOrEmpty(path))
            {
                throw new ArgumentNullException("path");
            }

            if (System.IO.Directory.Exists(path))
            {
                // Load any dynamic link libraries in this directory
                foreach (string file in Directory.GetFiles(path, @"*.dll"))
                {
                    try
                    {
                        this.LoadFile(file);
                    }
                    catch (BadImageFormatException ex)
                    {
                        Console.WriteLine("Unmanaged assembly failed to load: " + ex.Message);
                    }
                }

                // Load any sub-directories in this directory
                if (recursive)
                {
                    foreach (string directory in Directory.GetDirectories(path))
                    {
                        this.LoadDirectory(directory, recursive);
                    }
                }
            }
            else
            {
                throw new ArgumentException("directoryPath must be a valid path to a directory.", "path");
            }
        }

        /// <summary>
        /// Loads an IPlugin from the specified dynamic link library.
        /// </summary>
        /// <param name="filePath">The path to a dynamic link library.</param>
        /// <exception cref="System.ArgumentNullException">Thrown when <paramref name="filePath"/> is null.</exception>
        public void LoadFile(string filePath)
        {
            if (string.IsNullOrEmpty(filePath))
            {
                throw new ArgumentNullException("filePath");
            }

            // Create a new assembly from the plugin file
            Assembly assembly = Assembly.LoadFrom(filePath);

            // Loop through all the Types found in the assembly
            foreach (Type objectType in assembly.GetTypes())
            {
                // Look at public, non-abstract classes
                if (objectType.IsClass && objectType.IsPublic && !objectType.IsAbstract)
                {
                    // Make sure the object supports the interfaces in the filter
                    if (this.SupportsInterfaces(objectType))
                    {
                        // Create a new plugin
                        IPlugin plugin = (IPlugin)Activator.CreateInstance(assembly.GetType(objectType.ToString()));

                        if (!this.Plugins.ContainsKey(plugin.Name))
                        {
                            // Add plugin to collection
                            this.Plugins.Add(plugin.Name, plugin);

                            // Add listener for Unload event
                            plugin.PluginUnloadEvent += new EventHandler<PluginUnloadEventArgs>(this.PluginUnloadEventHandler);

                            // Initialize the plugin
                            plugin.Initialize();

                            // Raise event
                            this.CallPluginLoadedEvent(plugin);
                        }
                    }
                }
            }

            assembly = null;
        }

        /// <summary>
        /// Removes and disposes of a plugin by key from this collection.
        /// </summary>
        /// <param name="key">The key of the plugin to remove.</param>
        /// <exception cref="System.ArgumentNullException">Thrown when the <paramref name="key"/> is null.</exception>
        public void Unload(string key)
        {
            if (string.IsNullOrEmpty(key))
            {
                throw new ArgumentNullException("key");
            }

            if (this.Plugins.ContainsKey(key))
            {
                this.Unload(this.Plugins[key]);
            }
        }

        /// <summary>
        /// Removes and disposes a plugin from this collection.
        /// </summary>
        /// <param name="plugin">The plugin to remove.</param>
        /// <exception cref="System.ArgumentNullException">Thrown when the <paramref name="plugin"/> is null.</exception>
        /// <exception cref="System.ArgumentException">Thrown when the <paramref name="plugin"/> is not in the collection.</exception>
        public void Unload(IPlugin plugin)
        {
            if (plugin == null)
            {
                throw new ArgumentNullException("plugin");
            }

            plugin.PluginUnloadEvent -= new EventHandler<PluginUnloadEventArgs>(this.PluginUnloadEventHandler);
            this.Plugins.Remove(plugin.Name);
            this.CallPluginUnloadedEvent(plugin);
            plugin.Uninitialize();
        }

        /// <summary>
        /// Removes and disposes of all plugins from this collection.
        /// </summary>
        public void UnloadAll()
        {
            ICollection<IPlugin> plugins = new List<IPlugin>(this.Plugins.Values);
            foreach (IPlugin plugin in plugins)
            {
                this.Unload(plugin);
            }
        }

        /// <summary>
        /// Raises a PluginLoadedEvent for this PluginCollection.
        /// </summary>
        /// <param name="plugin">The plugin that was loaded.</param>
        /// <exception cref="System.ArgumentNullException">Thrown when the <paramref name="plugin"/> is null.</exception>
        protected void CallPluginLoadedEvent(IPlugin plugin)
        {
            if (this.PluginLoadedEvent != null)
            {
                this.PluginLoadedEvent(this, new PluginLoadedEventArgs(plugin));
            }
        }

        /// <summary>
        /// Raises a PluginUnloadedEvent for this PluginCollection.
        /// </summary>
        /// <param name="plugin">The plugin that was loaded.</param>
        /// <exception cref="System.ArgumentNullException">Thrown when the <paramref name="plugin"/> is null.</exception>
        protected void CallPluginUnloadedEvent(IPlugin plugin)
        {
            if (this.PluginUnloadedEvent != null)
            {
                this.PluginUnloadedEvent(this, new PluginUnloadedEventArgs(plugin));
            }
        }

        /// <summary>
        /// The handler for a PluginDisposedEvent raised by a plugin in this collection.
        /// </summary>
        /// <param name="sender">The object raising the event.</param>
        /// <param name="e">The arguments, containing the <code>IPlugin</code> that has requested to be unloaded.</param>
        private void PluginUnloadEventHandler(object sender, PluginEventArgs e)
        {
            this.Unload(e.Plugin);
        }

        /// <summary>
        /// Filters the collection on the current set of filters.
        /// </summary>
        private void Filter()
        {
            // Loop through all the objects in the the collection
            ICollection<IPlugin> plugins = new List<IPlugin>(this.Plugins.Values);
            foreach (IPlugin plugin in plugins)
            {
                // Get the plugin type
                Type pluginType = plugin.GetType();

                // Make sure the object supports the interfaces in the filter
                if (!this.SupportsInterfaces(pluginType))
                {
                    // Plugin does not support the interfaces
                    this.Unload(plugin);
                }

                pluginType = null;
            }
        }

        /// <summary>
        /// Resolves the type support of the interfaces required by this manager.
        /// </summary>
        /// <param name="type">The type to resolve.</param>
        /// <returns>True if the type supports all interfaces required by this manager, false otherwise.</returns>
        private bool SupportsInterfaces(Type type)
        {
            Type[] interfaces = type.GetInterfaces();
            foreach (Type interfaceType in this.PluginTypeFilter)
            {
                if (!interfaces.Contains(interfaceType))
                {
                    return false;
                }
            }

            return true;
        }
    }
}